Unity iOS/Androidの実行時に関数のswapを行いたかった


概要

結果的には、C#の実行環境?としてのIL2CPPの挙動と、C#元来の動作の違いを噛み締めることになった。

知れたので満足している。


なお別の方法で関数のスワップはできる。めっちゃ泥臭い方法で。

自分のやり方が間違っている可能性が強いので、記録までに。



試したこと

1.iOS/Androidでのポインタのスワップによる実行時メソッドの入れ替え -> 失敗

2.IL生成と実行 -> 失敗


結果

methodInfoからポインタを引っ張ってunsafeな感じで複数のメソッドのポインタをswapすることはできる。

が、


現状のIL2CPPだと、

・変わるのはmethodInfoのポインタのみ

・変えられるmethodInfoは対象メソッドが"一切"実行される前のもののみ


という制約にぶち当たった。



また、IL生成と実行はDelegateを作って実行しようとするとUnsupportedで詰む感じだった。


どういう制約だったか順に書いていく。



サンプルプロジェクト

一連の動作を試したサンプルプロジェクトを下記にアップした。

MainSceneというシーンでPlayすると、動作が行われる。

エディタと実機で動作が異なる。

https://github.com/sassembla/Pointers



1.iOS/Androidでの実行上のメソッドの入れ替え -> 失敗

次のようなコードを書く

public class BaseClass {

public void BaseMethod() {

Debug.LogError("BaseMethod.");

}

}


public class NewClass {

public void NewMethod() {

Debug.Log("NewMethod!");

}

}


// method for swapping method pointers.

var baseMethod_MethodInfo = typeof(BaseClass).GetMethod("BaseMethod");

var newMethod_MethodInfo = typeof(NewClass).GetMethod("NewMethod");


var baseIntPtr = baseMethod_MethodInfo.MethodHandle.Value;

var newIntPtr = newMethod_MethodInfo.MethodHandle.Value;


unsafe {

// ready void* pointers.

var basePointer = baseIntPtr.ToPointer();

var newPointer = newIntPtr.ToPointer();

// swap method pointer, *base ---> *new.

*((Int64*)basePointer) = *((Int64*)newPointer);

}


var baseInstance = new BaseClass();

baseInstance.BaseMethod();// エディタではNewMethodが実行されるが、実機では元のBaseMethodが実行される。。。


baseMethod_MethodInfo.Invoke(baseInstance, new object[]{});// この呼び方だと実機でもエディタでもNewMethodが実行される。



IL2CPP上だと、変わるのはmethodInfoのポインタのみ

GetMethodとかから取得したmethodInfoと、インスタンスが保持しているメソッドのポインタが異なるため、

swapさせても書き換わるのはmethodInfoの関数だけだった。

太字の箇所での呼び出しで、実機とエディタでは動作に差が出る。


これがエディタとかの環境だと、methodInfoから得たポインタに対してswapを行った時点で、既存のインスタンスのメソッドとかのポインタも全て書き換わる。


最初試した時は、swapしたポインタの元であるmethodInfoからメソッド実行をした際、swapを観測した時点で「おっこれいけるやんけ」とか思ったんだけど、

インスタンスからの実行だと旧来のメソッドの内容が呼ばれてしまう。

ログで見るとこんな感じ。


エディタ上

NewMethod!

UnityEngine.Debug:Log(Object)

BaseClass:BaseMethod() (at Assets/Pointers.cs:16)

Pointers:PointerSwap() (at Assets/Pointers.cs:45)

Pointers:Start() (at Assets/Pointers.cs:24)


NewMethod!

UnityEngine.Debug:Log(Object)

BaseClass:BaseMethod() (at Assets/Pointers.cs:16)

System.Reflection.MethodBase:Invoke(Object, Object[])

Pointers:PointerSwap() (at Assets/Pointers.cs:47)

Pointers:Start() (at Assets/Pointers.cs:24)


実機上(iOS)

BaseMethod.

Pointers:PointerSwap()

Pointers:Start()

 

(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 42)


NewMethod!

System.Reflection.MonoMethod:Invoke(Object, BindingFlags, Binder, Object[], CultureInfo)

Pointers:PointerSwap()

Pointers:Start()

 

(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 42)


赤いところがまあ、、問題なわけで。実機ではメソッド呼び出し先がswapされていない。



IL2CPP上だと、コンパイル済みの時点で、インスタンス化されてから参照されるクラス情報と、インスタンス化時のクラス情報とに接続がないとか

そういうんじゃねーかな~と思っている。methodHandleから取得したポインタへの操作が反映されないっていう。


もしくはポインタの位置が全然異なるとかなのかな。だったらそれ追えれば良いんだけど。


ちなみにUnity5.5b1まで試したけど、実行状態は変わらなかった。



まあゲームエンジンからC++のポインタぶっ叩いてるだけだからな、、関係性はある程度動けばそれで良いでしょ的な。

ナチュラルボーンC#erな人とかには耐え難い差になりそう。

このへん、.Net Foundationに入った今後、どうなるんだろう.



インスタンス自体のメソッドのポインタをすげ替えるっていうことをすればいけると思うが。

現状の仕組みの上で頑張るのなんかダメな努力な気がするんでパス。


この状態が限界なのか、それとも自分がどこかでミスをしているのか、、、気になる。


変えられるmethodInfoは対象メソッドが"一切"実行される前のもののみ

日本語が難しい。

methodInfoをIL2CPP上で扱うときに面白かったのが、「methodのswapが可能なのはそのmethodを一度でも実行する前」みたいな制約があることだった。

例えばmethodInfoを得る前に対象のmethodをどこかのインスタンスで実行してしまうと、そのmethodのswapは不可能になる。

swapを実行しても結果が出ない。


なんかまああれですよ、実行時にテーブル作ったりしてるんじゃない?って思っていた。

Unityエディタ上でも発生するので、なんかおもしろ制約があるんだろう。

これについては、swap実行前に対象のmethodを実行しない、という前提が作れれば回避することができた。

逆に言えば、この制約を回避できたせいで、「うおーーいけるやろーー」って思ってしまったのが悲しかった。



2.IL生成と実行 -> 失敗


IL生成まではできるんだけど実行のためのDelegateが作り出せない

これはもうIL生成時にはっきりエラーが出る箇所があって、

DynamicMethodクラスのCreateDelegateメソッドで即死する。


var dMethod = new DynamicMethod("test", typeof(void), new Type[]{});


var il = dMethod.GetILGenerator();

il.EmitWriteLine("hello!!");

il.Emit(OpCodes.Ret);


var testDelegate = (Test.TestDelegate)dMethod.CreateDelegate(typeof(Test.TestDelegate));

testDelegate();


とかやると、CreateDelegateの時点でunsurpported吐いて同期的に死ぬ。


testDelegate error:System.NotSupportedException: /Users/builduser/buildslave/unity/build/Tools/il2cpp/il2cpp/libil2cpp/icalls/mscorlib/System.Reflection.Emit/DynamicMethod.cpp(21) : Unsupported internal call for IL2CPP:DynamicMethod::create_dynamic_method - System.Reflection.Emit is not supported.

  at System.Reflection.Emit.DynamicMethod.CreateDynMethod () [0x00000] in <filename unknown>:0 

  at System.Reflection.Emit.DynamicMethod.CreateDelegate (System.Type delegateType) [0x00000] in <filename unknown>:0 

  at Project2501.Shadowing.Maining () [0x00000] in <filename unknown>:0 

Project2501.Shadowing:Maining()

太字の部分が主な死因。


で、これがちょっと曲者で、

実は

var dMethod = new DynamicMethod("test", typeof(void), new Type[]{});


この行の時点ですでに死が確定してて、上記の実行まで行っていようがいなかろうが、

Unhandled Exception: System.NotSupportedException: /Users/builduser/buildslave/unity/build/Tools/il2cpp/il2cpp/libil2cpp/icalls/mscorlib/System.Reflection.Emit/DynamicMethod.cpp(26) : Unsupported internal call for IL2CPP:DynamicMethod::destroy_dynamic_method - System.Reflection.Emit is not supported.

  at System.Reflection.Emit.DynamicMethod.Finalize () [0x00000] in <filename unknown>:0 

UnityEngine.UnhandledExceptionHandler:PrintException(String, Exception)

UnityEngine.UnhandledExceptionHandler:HandleUnhandledException(Object, UnhandledExceptionEventArgs)


が人知れず出る。

new DynamicMethod実行の行をtry-catchで囲もうが出るので、なんだろ、、うん、、、みたいな感じ。


つまりまあ無理だ。


ちなみにこのエラーがでてもアプリケーション自体は元気に動き続ける。

なぜなのか。


とりあえずこの方法での実行時IL生成は無理でした。というまとめまでに。



結論

IL生成は無理っぽいので諦めようと思っているのだけれど、swapは頑張ってみたい。

間違いが知りたい。



追記 swapに関して試して効果がなかったこと

・API Compatibilityを2.0にする